package com.youlu.campus.admin.auth.shiro.credentials;

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import com.youlu.campus.admin.auth.common.DeviceType;
import com.youlu.campus.admin.auth.entity.exception.AuthErrorCode;
import com.youlu.campus.admin.auth.service.AuthTokenService;
import com.youlu.campus.admin.auth.service.RoleService;
import com.youlu.campus.admin.auth.shiro.exception.AuthException;
import com.youlu.campus.admin.auth.shiro.token.AuthPrincipal;
import com.youlu.campus.admin.auth.shiro.token.AuthToken;
import io.jsonwebtoken.*;
import org.apache.commons.lang3.EnumUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.subject.SimplePrincipalCollection;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Base64Utils;

import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author zhuhuaiqi
 */
public class AuthCredentialsService {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());

    public static final String DEFAULT_KEY = AuthCredentialsService.class.getName();

    public static final int DEFAULT_EXPIRES_IN = 24 * 60 * 60 * 1000; //24小时

    public static final String PARAM_COM_ID = "comId";
    public static final String PARAM_UID = "uid";
    public static final String PARAM_DOMAIN = "domain";

    public static final String PARAM_ROLES = "roles";

    public static final String PARAM_DEVICE_TYPE = "deviceType";

    private String key = DEFAULT_KEY;

    private int expiresIn = DEFAULT_EXPIRES_IN;

    /**
     * 发行者
     */
    private String issuer;

    private Map<String, SecretKeySpec> secretKeySpecs = Maps.newHashMapWithExpectedSize(1);

    @Autowired
    private AuthTokenService authTokenService;

    @Autowired
    private RoleService roleService;

    public Key getSecretKeySpec(String key) {
//        logger.debug("getSecretKeySpec param key:{}", getKey());
        SecretKeySpec secretKeySpec = secretKeySpecs.get(key);
        if (secretKeySpec != null) {
            return secretKeySpec;
        }
        secretKeySpec = new SecretKeySpec(key.getBytes(), SignatureAlgorithm.HS512.getJcaName());
        secretKeySpecs.put(key, secretKeySpec);
//        logger.debug("secretKeySpec :{}", JSON.toJSONString(secretKeySpec));
        return secretKeySpec;
    }

    public String genAccessToken(AuthToken authToken) {
        long genAccessTime = System.currentTimeMillis();
        String jti = UUID.randomUUID().toString();
        List<String> roles = roleService.listRoleIdentifier(authToken.getUid(), authToken.getComId());

        String compactJws = Jwts.builder()
                .setSubject(authToken.getUsername())
                .setId(jti)
                .claim(PARAM_UID, String.valueOf(authToken.getUid()))
                .claim(PARAM_COM_ID, String.valueOf(authToken.getComId()))
                .claim(PARAM_DOMAIN, String.valueOf(authToken.getDomain()))
                .claim(PARAM_ROLES, roles)
                .claim(PARAM_DEVICE_TYPE, authToken.getDeviceType())
                //.setNotBefore(new Date(now))
                .setExpiration(new Date(genAccessTime + getExpiresIn()))
                .setIssuedAt(new Date(genAccessTime))
                .setIssuer(getIssuer())
                .signWith(SignatureAlgorithm.HS256, getSecretKeySpec(key))
                //.setExpiration(new Date(System.currentTimeMillis()+DEFAULT_EXPIRES_IN))
                .compact();

//        logger.debug("genAccessToken getExpiresIn:{}", JSON.toJSONString(getExpiresIn()));

        authToken.setJti(jti);
        authToken.setLastAccessTime(genAccessTime);
        authToken.setExpiresIn(getExpiresIn());
        authToken.setRoles(roles);
//        logger.debug("genAccessToken authToken:{}", JSON.toJSONString(authToken));
        authTokenService.withAuthTokenCache(authToken);
        //return compactJws;
        return Base64Utils.encodeToUrlSafeString(compactJws.getBytes());
    }


    public boolean checkAccessToken(byte[] bytes, AuthenticationInfo authenticationInfo, boolean updateTtl) {
        if (bytes == null) {
            return false;
        }
        //logger.debug("checkAccessToken authenticationInfo:{}", authenticationInfo);
        Jws<Claims> jws = null;
        boolean result = false;
        try {
            String credentials = new String(Base64Utils.decodeUrlSafe(bytes));
            jws = Jwts.parser().setSigningKey(getSecretKeySpec(key)).parseClaimsJws(credentials);

            jwtToAuthenticationInfo(jws.getBody(), authenticationInfo, updateTtl);
            result = true;
        } catch (UnsupportedJwtException | MalformedJwtException | IllegalArgumentException ex) {
            logger.warn("Invalid JWT Token {}", ex.getMessage());
            throw new AuthException(AuthErrorCode.INVALID_AUTH_TOKEN);
        } catch (ExpiredJwtException expiredEx) {
            logger.error("JWT Token is expired:过期用户重新登陆");
            jwtToAuthenticationInfo(expiredEx.getClaims(), authenticationInfo, updateTtl);
            result = true;

        } catch (AuthException be) {
            logger.warn("buinsess ex errorCode :{}", JSON.toJSONString(be.getErrorCode()));
            throw new AuthException(be.getErrorCode());
        } catch (Exception e) {
            logger.error("unknown ex", e);
            throw new AuthException(AuthErrorCode.USER_NOT_LOGIN);
        }
        return result;
    }

    private void jwtToAuthenticationInfo(Claims jwsBody, AuthenticationInfo authenticationInfo, boolean updateTtl) {
        String id = jwsBody.getId();
        String uid = jwsBody.get(PARAM_UID, String.class);
//        String uid = NumberUtils.toLong(uidStr, -1);
        Boolean result = authTokenService.checkAuthTokenCache(uid, id, updateTtl);

        logger.info(":>>> jwtToAuthenticationInfo result 结果:{}", result);
        if (!result) {
            throw new AuthException(AuthErrorCode.USER_NOT_LOGIN);
        }

        SimpleAuthenticationInfo info = (SimpleAuthenticationInfo) authenticationInfo;
        AuthPrincipal authPrincipal = new AuthPrincipal();

        authPrincipal.setLoginName(jwsBody.getSubject());

        authPrincipal.setUid(jwsBody.get(PARAM_UID, String.class));
        authPrincipal.setComId(jwsBody.get(PARAM_COM_ID, String.class));
        authPrincipal.setDomain(jwsBody.get(PARAM_DOMAIN, String.class));

        //logger.debug("jwtToAuthenticationInfo role:{}", jwsBody.get(PARAM_ROLES, List.class));
        //String rolesStr = jwsBody.get(PARAM_ROLES, String.class);
        List<String> rolesList = (List<String>) jwsBody.get(PARAM_ROLES);
        logger.info("【获取角色】roles -> {}",rolesList.size());
        authPrincipal.setRoles(rolesList.stream().collect(Collectors.toSet()));
        /*long expiration = jwsBody.getExpiration().getTime();

        long issuedAt = jwsBody.getIssuedAt().getTime();

        int expiresIn = new Long(expiration - issuedAt).intValue();
        authPrincipal.setGmtCreate(issuedAt);
        authPrincipal.setExpiresIn(expiresIn);*/

        DeviceType deviceType = EnumUtils.getEnum(DeviceType.class,
                jwsBody.get(PARAM_DEVICE_TYPE, String.class));
        authPrincipal.setDeviceType(deviceType);

        Set<String> realmNames = info.getPrincipals().getRealmNames();
        String realmNameStr = null;
        for (String realmName : realmNames) {
            realmNameStr = realmName;
        }
//        logger.debug("jwtToAuthenticationInfo authPrincipal:{}, realmNameStr:{}", JSON.toJSONString(authPrincipal), realmNameStr);
        info.setPrincipals(new SimplePrincipalCollection(authPrincipal, realmNameStr));
    }

    public String getKey() {
        return key;
    }

    public void setKey(String key) {
        this.key = key;
    }

    public int getExpiresIn() {
        return expiresIn;
    }

    public void setExpiresIn(int expiresIn) {
        this.expiresIn = expiresIn;
    }

    public String getIssuer() {
        return issuer;
    }

    public void setIssuer(String issuer) {
        this.issuer = issuer;
    }

}
